Resilience4j 从入门到放弃

作者: 李多多 日期: 2020-08-11
Spring Cloud
Resilience4j 从入门到放弃

@TOC

简介

Resilience4j 是 Spring Cloud Greenwich 版推荐的容错解决方案,相比 Hystrix ,Resilience4j 专为 Java8 以及函数编程而设计。在 Resilience4j 中你用什么可直接添加什么以来就行。
Resilience4j 主要有以下功能:

  1. CircuitBreaker(熔断器)
  2. RateLimiter(限流)
  3. Retry(请求重试)
  4. 限时
  5. 缓存
  6. 信号量的隔离

1.基本用法

首先搭建一个测试环境。新建maven子模,导入pom

<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
</dependency>

1.1熔断器

添加断路器依赖:

<dependency>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-circuitbreaker</artifactId>
<version>0.13.2</version>
</dependency>

1.1.1 首先看一个正常的断路器:

@Test
public void test1() {
//获取一个CircuitBreakerRegistry实例,可以调用ofDefaults获取一个CircuitBreakerRegistry实例,也可以自定义属性。
CircuitBreakerRegistry registry = CircuitBreakerRegistry.ofDefaults();
CircuitBreakerConfig config = CircuitBreakerConfig.custom()
//故障率阈值百分比(百分之50),超过这个阈值,断路器就会打开
.failureRateThreshold(50)
//断路器保持打开的时间,在到达设置的时间之后,断路器会进入到 half open 状态
.waitDurationInOpenState(Duration.ofMillis(1000))
//当断路器处于half open 状态时,环形缓冲区的大小
.ringBufferSizeInHalfOpenState(2)
.ringBufferSizeInClosedState(2)
.build();
CircuitBreakerRegistry r1 = CircuitBreakerRegistry.of(config);
CircuitBreaker cb1 = r1.circuitBreaker("javaboy");//创建断路器
CircuitBreaker cb2 = r1.circuitBreaker("javaboy2", config);
CheckedFunction0<String> supplier = CircuitBreaker.decorateCheckedSupplier(cb1, () -> "hello resilience4j");
Try<String> result = Try.of(supplier)
.map(v -> v + " hello world");
System.out.println(result.isSuccess());
System.out.println(result.get());
}

在这里插入图片描述

1.1.2 一个异常的断路器:

public void test2() {
CircuitBreakerConfig config = CircuitBreakerConfig.custom()
//故障率阈值百分比,超过这个阈值,断路器就会打开
.failureRateThreshold(50)
//断路器保持打开的时间,在到达设置的时间之后,断路器会进入到 half open 状态
.waitDurationInOpenState(Duration.ofMillis(1000))
//当断路器处于half open 状态时,环形缓冲区的大小
.ringBufferSizeInClosedState(2)
.build();
CircuitBreakerRegistry r1 = CircuitBreakerRegistry.of(config);
CircuitBreaker cb1 = r1.circuitBreaker("javaboy");
System.out.println(cb1.getState());//获取断路器的一个状态
cb1.onError(0, new RuntimeException());
System.out.println(cb1.getState());//获取断路器的一个状态
cb1.onError(0, new RuntimeException());
System.out.println(cb1.getState());//获取断路器的一个状态


在这里插入图片描述
把这段代码添加到上面代码的后面,重新测试。

...   
CheckedFunction0<String> supplier = CircuitBreaker.decorateCheckedSupplier(cb1, () -> "hello resilience4j");
Try<String> result = Try.of(supplier)
.map(v -> v + " hello world");
System.out.println("result.isSuccess()="+result.isSuccess());
System.out.println("result.get()="+result.get());
}

如下(由于断路器已经打开了,就无法执行下面的代码了):
在这里插入图片描述

注意:由于 ringBufferSizeInClosedState 的值为2,表示当有两条数据时才会去统计故障率,所以,下面的手动故障测试,至少调用两次 onError,断路器才会打开。

1.1.3 断路器重置

circuitBreaker.reset();

1.2 RateLimiter 限流

RateLimiter 本身和前面的断路器差不多。

/**
* 限流 和断路器类似
*/
@Test
public void test3(){
RateLimiterConfig build = RateLimiterConfig.custom()
// 阈值刷新的时间 1 秒
.limitRefreshPeriod(Duration.ofMillis(1000))
// 限制频次
.limitForPeriod(2)
// 限流之后的冷却时间 1秒
.timeoutDuration(Duration.ofMillis(1000))
.build();
RateLimiter limiter = RateLimiter.of("learning", build);

CheckedRunnable runnable = RateLimiter.decorateCheckedRunnable(limiter, () -> {
System.out.println(new Date());
});

// 执行4次
Try.run(runnable)
.andThenTry(runnable)
.andThenTry(runnable)
.andThenTry(runnable)
.onFailure(t -> System.out.println(t.getMessage()));
}

在这里插入图片描述

1.3 请求重试

先引入依赖:

<dependency>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-retry</artifactId>
<version>0.13.2</version>
</dependency>
 /**
* 请求重试
*/
@Test
public void test4(){
RetryConfig config = RetryConfig.custom()
// 重试次数 【小于等于下面的count次数就抛出异常】
.maxAttempts(4)
// 重试间隔
.waitDuration(Duration.ofMillis(500))
// 重试异常
.retryExceptions(RuntimeException.class)
.build();

Retry retry = Retry.of("leaning1", config);
Retry.decorateRunnable(retry, new Runnable() {
int count = 0;
// 重试功能开启后 执行run方法 若抛出异常 会自动触发重试功能
@Override
public void run() {
if (count++ < 4){
System.out.println(count);
throw new RuntimeException();
}
}
}).run();
}

在这里插入图片描述

2. Resilience4j 结合微服务

Retry、CircuitBreaker、RateLimiter

2.1 Retry 请求重试

首先新建一个Spring boot 项目,创建时,添加如下依赖:
在这里插入图片描述
创建成功后再添加 Resilience4j 的依赖如下:

 <dependency
<!--resilience4j-spring-boot2中包含了resilience4j 的所有功能,但是没有配置的功能无法使用,需要将之从依赖中剔除掉 -->
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-spring-boot2</artifactId>
<version>1.2.0</version>
<exclusions>
<exclusion>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-circuitbreaker</artifactId>
</exclusion>

<exclusion>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-ratelimiter</artifactId>
</exclusion>
<exclusion>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-bulkhead</artifactId>
</exclusion>
<exclusion>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-timelimiter</artifactId>
</exclusion>
</exclusions>
</dependency>

resilience4j-spring-boot2中包含了resilience4j 的所有功能,但是没有配置的功能无法使用,需要将之从依赖中剔除掉。
配置yml文件:

resilience4j:
retry:
retry-aspect-order: 399 # 表示Retry优先级(级别高于比如ratelimiter bulkhead timelimiter) 值越小 优先级 越高
backends:
retryA: # 设置组名
maxRetryAttempts: 5 # 对比之前的案例 重试的次数
waitDuration: 500 # 重试等待 500毫秒
exponentialBackoffMultiplier: 1.1 # 间隔乘数(场景: 正好每次间隔为1的时候卡顿 它就有用了 间隔就变了 例如 1 1.1 1.21....)
retryExceptions:
- java.lang.RuntimeException
spring:
application:
name: resilience4j
server:
port: 5000
eureka:
client:
service-url:
defaultZone: http://localhost:1111/eureka

最后创建RestTemplate和HelloService:

@SpringBootApplication
public class Resilience4j2Application {

public static void main(String[] args) {
SpringApplication.run(Resilience4j2Application.class, args);
}

@Bean
RestTemplate restTemplate(){
return new RestTemplate();
}
}



@Service
@Retry(name = "retryA")//表示要使用的重试策略
public class HelloService {
@Autowired
RestTemplate restTemplate;

public String hello(){
return restTemplate.getForObject("http://127.0.0.1:1113/hello", String.class);
}
}


@RestController
public class HelloController {
@Autowired
HelloService helloService;

@GetMapping("/hello")
public String hello(){
return helloService.hello();
}
}

浏览器访问: http:/127.0.0.1:5000/hello ,观看 provider 控制台,发现错误打印了5次,与yml中的配置一致。

2.2 CircuitBreaker 断路器

首先去除掉circuitbreaker 的依赖。然后在yml 文件中配置:

<dependency>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-spring-boot2</artifactId>
<version>1.2.0</version>
<exclusions>
<!-- 没有配置的 先排除 不然会报错 -->
<!-- <exclusion>-->
<!-- <groupId>io.github.resilience4j</groupId>-->
<!-- <artifactId>resilience4j-circuitbreaker</artifactId>-->
<!-- </exclusion>-->

<exclusion>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-ratelimiter</artifactId>
</exclusion>
<exclusion>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-bulkhead</artifactId>
</exclusion>
<exclusion>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-timelimiter</artifactId>
</exclusion>
</exclusions>
</dependency>

resilience4j:
retry:
retry-aspect-order: 399 # 表示Retry优先级(级别高于比如ratelimiter bulkhead timelimiter) 值越小 优先级 越高
backends:
retryA: # 设置组名
maxRetryAttempts: 5 # 对比之前的案例 重试的次数
waitDuration: 500 # 重试等待 500毫秒
exponentialBackoffMultiplier: 1.1 # 间隔乘数(场景: 正好每次间隔为1的时候卡顿 它就有用了 间隔就变了 例如 1 1.1 1.21....)
retryExceptions:
- java.lang.RuntimeException
circuitbreaker: # 和之前的maven类似
instances:
cba:
ringBufferSizeInHalfOpenState: 3
ringBufferSizeInClosedState: 5
waitInterval: 5000
recordExceptions:
- org.springframework.web.client.HttpServerErrorException
circuit-breaker-aspect-order: 398 # 表示 circuitbreaker 优先级,比上面的399小 (先执行当前断路器)
spring:
application:
name: resilience4j
server:
port: 5000
eureka:
client:
service-url:
defaultZone: http://localhost:1111/eureka

配置完成后,用 @CircuitBreaker 注解标记相关方法:

@Service
//@Retry(name = "retryA")//【Retry】表示要使用的重试策略
@CircuitBreaker(name = "cba" , fallbackMethod = "error")
public class HelloService {
@Autowired
RestTemplate restTemplate;

public String hello(){
return restTemplate.getForObject("http://127.0.0.1:1113/hello", String.class);
}

// 服务降级方法中 不加参数Throwable 会报错提示缺少Throwable 要添加异常参数
public String error(Throwable throwable){
return "error";
}
}

@CircuitBreaker 注解中的 name 属性用来指定 circuitbreaker 配置 , fallbackMethod 属性用来指定服务降级的方法,需要注意的是,服务降级方法中,要添加异常参数 (Throwable)。

2.3 RateLimiter 限流

RateLimiter 作为限流工具,主要在服务端使用,用来保护服务端的接口。
首先在 provider 中添加RateLimiter 依赖:

      <dependency>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-spring-boot2</artifactId>
<version>1.2.0</version>
<exclusions>
<!-- 没有配置的 先排除 不然会报错 -->
<exclusion>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-circuitbreaker</artifactId>
</exclusion>

<!--ratelimiter依赖移除-->
<!-- <exclusion>-->
<!-- <groupId>io.github.resilience4j</groupId>-->
<!-- <artifactId>resilience4j-ratelimiter</artifactId>-->
<!-- </exclusion>-->
<exclusion>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-bulkhead</artifactId>
</exclusion>
<exclusion>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-timelimiter</artifactId>
</exclusion>
</exclusions>
</dependency>

接下来,在 provider 的 application.properties 配置文件中去配置 RateLimiter:

# 这里配置每一秒处理一个请求,多个请求就排队
resilience4j.ratelimiter.limiters.rlA.limit-for-period=1
resilience4j.ratelimiter.limiters.rlA.limit-refresh-period=1s
resilience4j.ratelimiter.limiters.rlA.timeout-duration=1s

配置 provider 的 Controller 层

@RestController
public class HelloController {


@Value("${server.port}")
Integer port;

@GetMapping("/hello")
@RateLimiter(name = "rltA") //通过@RateLimiter注解标记该接口限流
public String hello(){
System.out.println(new Date());
return "hello provider:" + port;
}
}

接着在 resilience4j2 客户端模拟多个请求,启动 eureka、provider 、resilience4j 服务,访问 http://127.0.0.1:5000/hello1 查看限流效果。如下:


@RestController
public class HelloController {
@Autowired
HelloService helloService;

@GetMapping("/hello")
public String hello(){
return helloService.hello();
}
}


@Servic
@CircuitBreaker(name = "cba" , fallbackMethod = "error")
public class HelloService {
@Autowired
RestTemplate restTemplate;

// 限流配置 【模拟多次请求】
public String hello(){
for (int i = 0; i < 5; i++) {
restTemplate.getForObject("http://127.0.0.1:1113/hello", String.class);
}
return "success ratA";
}
}

每秒处理一个请求。
在这里插入图片描述